Java从诞生之初就支持语言级并行编程,例如内建的java.lang.Thread类是对操作系统中线程的抽象,关键字synchronized和方法wait notify用于同步操作,这在当时是独一无二的,至少在学术界以外是这样的。在那时,商界编程语言还是依赖于操作系统提供的支持库来使用线程,而Java提供了平台无关的方式来操作线程,与以往相比,可谓是一大突破。
就同步操作来说,Java做的非常好,不仅是因为它显式的支持线程、锁和信号量,而且其内建机制使每个对象都可以作为监视器使用。在Java 1.5中,引入了java.util.concurrent包,其中包含了很多有设计精巧的、可用于并行编程的数据结构。
监视器(monitor)用于对需要同步的资源进行加锁,每次只能有一个线程持有该监视器,因此可以实现对资源的排他性访问。
这种设计的优势很明显,同步操作无需涉及第三方库的调用,而且可以使锁具有完整的定义,在编程的时候便于使用。
硬要说缺点的话,就是使用起来太容易了,可能有些人会滥用同步操作,结果导致应用程序的整体性能大幅下降。
当然,在具体实现上面还有一些可优化的地方。由于每个对象都可以作为监视器使用,每个对象都持有同步操作相关的信息,例如当前对象是否作为锁使用,锁的具体实现等。一般情况下,为了便于快速访问,这些信息被保存在每个对象的对象头(object hreader)的锁字(lock word)中。对于自动内存管理来说,多线程操作的性能优化问题同样存在,必须要能够快速获取到垃圾回收信息,例如某个对象的垃圾回收状态,在第三章介绍引用跟踪垃圾回收时提到的过标记位(mark bit)就表示了这类信息。JRockit使用锁字中的一些位来存储垃圾回收状态信息。虽然其中包含了垃圾回收信息,不过在本书中还是称之为 锁字。
如果将对象头中存储的信息过度编码的话,那么在使用的时候,就不得不花额外的力气去解码;如果不经编码直接存储,又会消耗大量的内存,因此,在存储每个对象的锁信息和垃圾回收信息时,需要仔细权衡。
对象头中还包含了指向类型信息的指针,在JRockit中,这称为 类块(class block)。
下图是JRockit中Java对象在不同CPU平台上的内存布局。为了节省内存,并加速解引用操作,对象头中所有的 字(word)的宽度都是32位。类块是一个32位的指针,指向另一个外部结构,其中该结构中包含了当前对的类型信息和 虚分派表(virtual dispatch table)等信息。

就目前所知,绝大部分JVM中,对象头是使用两个32位长的字来表示的。在JRockit中,偏移为0的对象指针指向当前对象的类型信息,接下来是4个字节的锁字。在SPARC平台上,对象头的布局刚好反过来,因为在使用原子指令在操作指针时,如果没有偏移的话,效率会更好一点。与锁字不同,类块并不是为原子操作所使用,因此在SPARC平台上,类块被放在锁字后面。
原子操作是指一个全部执行或全部不执行的本地指令。当原子指令全部执行时,其操作结果需要对所有潜在访问者可见。
原子操作用于读写 锁字,具有排他性,这是实现JVM中同步块的基础。
研究表明,在前的基础上,再压缩对象头(例如将之压缩为单个32位的字),已经没什么意义了,即使可以节省出更多的内存,但在使用的时候,需要额外的解码操作,得不偿失,
对于大多数平台和编程语言来说,光是并发带来的问题就已经足够多了,例如 死锁(dealock) 活锁和各种奇妙的崩溃等。由于并发问题往往与时序相关,所以想要重现问题也不是那么容易。附加调试器后,会增加额外的开销,导致问题重现更加困难。
死锁是指两个线程都在等在对象释放自己所需的资源,结果导致两个线程都进入休眠状态。很明显,它们再也醒不过来了。活锁的概念与死锁类似,区别在于线程在竞争时会采取主动操作,但却导致无法获取到锁。举例来说,两个人面对面前进,在一个很窄的走廊相遇,为了能继续前进,他们都向侧面移动,但由于移动的方向相反,导致还是无法前进。
由于存在着上面提到的这些问题,调试并行系统是件非常困难的任务,一些可视化工具和调试器可以帮助解开线程间的锁依赖,这对并发程序调试来说,已经是巨大的帮助了。
像其他主流JVM一样,JRockit可以在控制台里输出当前应用程序中所有线程的调用栈,并打印锁的持有者信息。对于简单的死锁问题来说,这些信息已经足够用来解决问题了。本章后续内容会对此做举例说明。
JRockit Mission Control套件中提供了可视化组件来显示线程的锁信息。
除了难以调试外,并行编程中使用的锁还极大的降低了应用程序的整体性能。每个锁都是一个性能瓶颈,它保证了关键区域的排他性访问,但却使得没有获取到锁的线程不得不等待执行的机会。如果锁放置错位,或者控制的关键区过大,就会导致应用程序性能大幅下降。
不幸的是,很多商业软件的性能问题就是一两个锁使用不当导致的。以往在调试第三方应用程序时曾多次见到这种情况,开发人员本身也没有意识到对锁的错误使用,幸运的是,使用不当的锁没几个,可以辨别出来,延迟问题比较容易解决。使用JRockit Mission Control可以很容易找到竞争最激烈的锁,编译排查问题。
锁 竞争激烈是指多个线程花费大量时间试图获取到某个锁。
JRockit Mission Control套件中孚延迟分析组件可以记录Java应用程序的运行信息,提供可视化的延迟分析数据。在优化大量使用同步操作的应用程序时,延迟分析可以给程序员带来很大的帮助。以往的分析工具只是展示了应用程序将时间花在了什么地方,而延迟分析仪则可以给出应用程序没有花时间在什么地方,当应用程序线程没有执行Java代码时,就将之记录到线程图中,这样就可以判断出,线程是在等待I/O,还是在等待获取锁。
后面的章节将详细介绍如何使用JRockit Mission Control中的延迟分析仪,主要在第8章 Runtime Analyzer和第9章 Flight Recorder。
下图是JRockit运行时分析仪中延迟分析标签页的内容,其数据来自于一个正在运行的服务器端应用程序,用于线下测试。图中的横线标明了应用程序线程都将时间花在了何处,每当出现新类型的延迟时,就使用一种不同的颜色来标明。在图中,线程绝大部分都是红色的,表明线程 阻塞在Java中,这很糟,它表明应用程序将时间都花在了等待Java锁上,例如获取同步块的锁。为了能够更加准确的表示线程的延迟情况,非绿色的线段表示"没有在执行Java代码",这其中可能包括了正在等待I/O、网络通信等资源的本地线程。

回忆一下在第3章 内存管理中对延迟的讨论,如果JVM将时间都花在垃圾回收上,就没办法执行Java代码了。类似的,如果CPU资源都浪费在等待I/O或Java锁上,就会导致延迟大幅提升,这也是大部分性能问题的根本原因。
JRockit Flight Recorder套件可以帮助定位Java程序中造成延迟的问题点。在上面的例子中,延迟的根源在于日志模块中存在对锁的不当使用。